chylex / Discord-History-Tracker

Desktop app & browser script that saves Discord chat history into a file, and an offline viewer that displays the file.
https://dht.chylex.com
MIT License
475 stars 83 forks source link

tracking messages fails with error #177

Closed SageSystems closed 2 years ago

SageSystems commented 2 years ago

error content: [ERROR] [BaseEndpoint] Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite Error 19: 'UNIQUE constraint failed: attachments.attachment_id'. [ERROR] [BaseEndpoint] at Microsoft.Data.Sqlite.SqliteException.ThrowExceptionForRC(Int32 rc, sqlite3 db) [ERROR] [BaseEndpoint] at Microsoft.Data.Sqlite.SqliteDataReader.NextResult() [ERROR] [BaseEndpoint] at Microsoft.Data.Sqlite.SqliteCommand.ExecuteReader(CommandBehavior behavior) [ERROR] [BaseEndpoint] at Microsoft.Data.Sqlite.SqliteCommand.ExecuteNonQuery() [ERROR] [BaseEndpoint] at DHT.Server.Database.Sqlite.SqliteDatabaseFile.AddMessages(Message[] messages) [ERROR] [BaseEndpoint] at DHT.Server.Endpoints.TrackMessagesEndpoint.Respond(HttpContext ctx) [ERROR] [BaseEndpoint] at DHT.Server.Endpoints.BaseEndpoint.Handle(HttpContext ctx)

error only happens at a certain point of a certain channel in a specific server, and nowhere else, when you scroll beyond the problem case messages tracking resumes as usual. the error seems to be related to attachments, this channel is full of them

SageSystems commented 2 years ago

The server is mostly open and verification is extremely easy, if anybody wants to test, you may ask me

chylex commented 2 years ago

Can you please invite chylexalt#2952?

Alternatively, you could try tracking the channel, and when the error happens, paste this code into the console and run it:

(function() {
class DISCORD{static getMessageOuterElement(){return DOM.queryReactClass("messagesWrapper")}static getMessageScrollerElement(){return DOM.queryReactClass("scroller",this.getMessageOuterElement())}static getMessageElements(){return this.getMessageOuterElement().querySelectorAll("[class*='message-']")}static hasMoreMessages(){return document.querySelector("#messagesNavigationDescription + [class^=container]")===null}static loadOlderMessages(){const view=this.getMessageScrollerElement();if(view.scrollTop>0){view.scrollTop=0}}static setupMessageCallback(callback){let skipsLeft=0;let waitForCleanup=false;const previousMessages=new Set;const timer=window.setInterval(()=>{if(skipsLeft>0){--skipsLeft;return}const view=this.getMessageOuterElement();if(!view){skipsLeft=2;return}const anyMessage=DOM.queryReactClass("message",this.getMessageOuterElement());const messageCount=anyMessage?anyMessage.parentElement.children.length:0;if(messageCount>300){if(waitForCleanup){return}skipsLeft=3;waitForCleanup=true;window.setTimeout(()=>{const view=this.getMessageScrollerElement();view.scrollTop=view.scrollHeight/2},1)}else{waitForCleanup=false}const messages=this.getMessages();const hasChanged=messages.some(message=>!previousMessages.has(message.id))||!this.hasMoreMessages();if(!hasChanged){return}previousMessages.clear();for(const message of messages){previousMessages.add(message.id)}callback(messages)},200);window.DHT_ON_UNLOAD.push(()=>window.clearInterval(timer))}static getMessageElementProps(ele){const props=DOM.getReactProps(ele);if(props.children&&props.children.length>=4){const childProps=props.children[3].props;if("message"in childProps&&"channel"in childProps){return childProps}}return null}static getMessages(){try{const messages=[];for(const ele of this.getMessageElements()){const props=this.getMessageElementProps(ele);if(props!=null){messages.push(props.message)}}return messages}catch(e){console.error(e);return[]}}static getSelectedChannel(){try{let obj;try{for(const child of DOM.getReactProps(DOM.queryReactClass("chatContent")).children){if(child&&child.props&&child.props.channel){obj=child.props.channel;break}}}catch(e){console.error("[DHT] Error retrieving selected channel from 'chatContent' element.",e);for(const ele of this.getMessageElements()){const props=this.getMessageElementProps(ele);if(props!=null){obj=props.channel;break}}}if(!obj||typeof obj.id!=="string"){return null}const dms=DOM.queryReactClass("privateChannels");if(dms){let name;for(const ele of dms.querySelectorAll("[class*='channel-'] [class*='selected-'] [class^='name-'] *, [class*='channel-'][class*='selected-'] [class^='name-'] *")){const node=Array.prototype.find.call(ele.childNodes,node=>node.nodeType===Node.TEXT_NODE);if(node){name=node.nodeValue;break}}if(!name){return null}let type;switch(obj.type){case 1:type="DM";break;case 3:type="GROUP";break;default:return null}const id=obj.id;const server={id:id,name:name,type:type};const channel={id:id,name:name};return{server:server,channel:channel}}else if(obj.guild_id){const server={id:obj.guild_id,name:document.querySelector("nav header h1[class*='name-']").innerText,type:"SERVER"};const channel={id:obj.id,name:obj.name,extra:{nsfw:obj.nsfw}};if(obj.parent_id){channel["extra"]["parent"]=obj.parent_id}else{channel["extra"]["position"]=obj.position;channel["extra"]["topic"]=obj.topic}return{server:server,channel:channel}}else{return null}}catch(e){console.error("[DHT] Error retrieving selected channel.",e);return null}}static selectNextTextChannel(){const dms=DOM.queryReactClass("privateChannels");if(dms){const currentChannel=DOM.queryReactClass("selected",dms);const currentChannelContainer=currentChannel&&currentChannel.closest("[class*='channel-']");const nextChannel=currentChannelContainer&&currentChannelContainer.nextElementSibling;if(!nextChannel||!nextChannel.getAttribute("class").includes("channel-")){return false}const nextChannelLink=nextChannel.querySelector("a[href*='/@me/']");if(!nextChannelLink){return false}nextChannelLink.click();nextChannelLink.scrollIntoView(true);return true}else{const channelListEle=document.getElementById("channels");if(!channelListEle){return false}function getLinkElement(channel){return channel.querySelector("a[href^='/channels/'][role='link']")}const allTextChannels=Array.prototype.filter.call(channelListEle.querySelectorAll("[class*='containerDefault']"),ele=>getLinkElement(ele)!==null);let nextChannel=null;for(let index=0;index<allTextChannels.length-1;index++){if(allTextChannels[index].className.includes("selected-")){nextChannel=allTextChannels[index+1];break}}if(nextChannel===null){return false}const nextChannelLink=getLinkElement(nextChannel);if(!nextChannelLink){return false}nextChannelLink.click();nextChannel.scrollIntoView(true);return true}}}
class DOM{static id(id,parent){return(parent||document).getElementById(id)}static queryReactClass(cls,parent){return(parent||document).querySelector(`[class*="${cls}-"]`)}static createElement(tag,parent,id,html){const ele=document.createElement(tag);ele.id=id||"";ele.innerHTML=html||"";parent.appendChild(ele);return ele}static removeElement(ele){return ele.parentNode.removeChild(ele)}static createStyle(styles){return this.createElement("style",document.head,"",styles)}static saveToCookie(name,obj,expiresInSeconds){const expires=new Date(Date.now()+1e3*expiresInSeconds).toUTCString();document.cookie=name+"="+encodeURIComponent(JSON.stringify(obj))+";path=/;expires="+expires}static loadFromCookie(name){const value=document.cookie.replace(new RegExp("(?:(?:^|.*;\\s*)"+name+"\\s*\\=\\s*([^;]*).*$)|^.*$"),"$1");return value.length?JSON.parse(decodeURIComponent(value)):null}static getReactProps(ele){const keys=Object.keys(ele||{});let key=keys.find(key=>key.startsWith("__reactInternalInstance"));if(key){return ele[key].memoizedProps}key=keys.find(key=>key.startsWith("__reactProps$"));return key?ele[key]:null}}

console.info(JSON.stringify(DISCORD.getMessages()));
})();

This will export the raw data of currently loaded messages, which should include the messages that are causing the error. You can copy the long text the console spits out into a text file and email it to contact@chylex.com, or DM it to the Discord account I mentioned earlier. Thanks.

chylex commented 2 years ago

The only way this error should happen is if two messages have the exact same attachment, including Discord's internal attachment ID. I don't know how this can happen, because if I upload the same file twice, each attachment gets a different IDs.

chylex commented 2 years ago

Fixed in version 35.2.