converts irc logs to html files that look like discord
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

223 lines
5.5 KiB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
  1. import { escapeHtmlShitty, HtmlEnt, JSX } from './jsx';
  2. const { readFileSync } = require("fs");
  3. const contents = readFileSync(0);
  4. const lines: string[] = contents.toString().split('\n');
  5. type MessageFrag = {
  6. type: 'url',
  7. link: string
  8. } | {
  9. type: 'text',
  10. content: string
  11. };
  12. type IrcUser = {
  13. mode?: '+' | '@' | ' ',
  14. name: string
  15. };
  16. type IrcMessage = {
  17. time: string,
  18. type: 'status',
  19. content: MessageFrag[]
  20. } | {
  21. time: string,
  22. type: 'action',
  23. user: IrcUser,
  24. content: MessageFrag[]
  25. } | {
  26. time: string,
  27. type: 'message',
  28. user: IrcUser,
  29. content: MessageFrag[]
  30. };
  31. const URL = /(((?:https?|gopher):\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/|spotify:track:)((?:\(?[^\s()<>]+\)?)*[^ \s`!\[\]{};:\'".,<>?\xab\xbb\u201c\u201d\u2018\u2019]))/i;
  32. function parseMessage(m: string): MessageFrag[] {
  33. m = m.trimEnd();
  34. const parts: MessageFrag[] = [];
  35. let match: RegExpMatchArray | null;
  36. while ((match = m.match(URL)) != null) {
  37. parts.push({ type: 'text', content: m.slice(0, match.index) });
  38. parts.push({ type: 'url', link: match[0] });
  39. m = m.slice((match.index ?? 0) + match[0].length);
  40. }
  41. if (m !== "") {
  42. parts.push({ type: 'text', content: m });
  43. }
  44. return parts;
  45. }
  46. function parse(s: string): IrcMessage {
  47. s = s.trimEnd();
  48. const time = s.slice(0, 5);
  49. switch (s[6]) {
  50. case '-':
  51. return {
  52. time,
  53. type: 'status',
  54. content: parseMessage(s.slice(10))
  55. };
  56. case '<': {
  57. const mode = s[7];
  58. if (mode !== '@' && mode !== '+' && mode !== ' ') throw `invalid mode: ${mode}`;
  59. const name = s.slice(8).split('>')[0];
  60. return {
  61. time,
  62. type: 'message',
  63. user: { mode, name },
  64. content: parseMessage(s.slice(10 + name.length))
  65. }
  66. }
  67. case ' ': {
  68. const name = s.slice(8).split(' ')[1];
  69. return {
  70. time,
  71. type: 'action',
  72. user: { name },
  73. content: parseMessage(s.slice(10 + name.length))
  74. }
  75. }
  76. }
  77. throw `couldn't parse message ${s}`;
  78. };
  79. const messages = lines.map(l => l.trim()).filter(l => l !== '.' && l !== "").map((v, i) => { try { return parse(v) } catch (e) { throw `${e} ${i}` } });
  80. type MsgGroup = {
  81. user?: IrcUser,
  82. type: "status" | "message" | "action",
  83. messages: IrcMessage[]
  84. }
  85. function group(messages: IrcMessage[]): MsgGroup[] {
  86. const groups: MsgGroup[] = [];
  87. let group: IrcMessage[] = [];
  88. let discrim: IrcUser | undefined = undefined;
  89. let type: "status" | "action" | "message" | undefined = undefined;
  90. for (const m of messages) {
  91. if (m.type === "status") {
  92. groups.push({
  93. user: discrim,
  94. type: type ?? "message",
  95. messages: group
  96. });
  97. discrim = undefined;
  98. type = "status";
  99. group = [];
  100. groups.push({
  101. type: m.type,
  102. messages: [m]
  103. })
  104. continue;
  105. } else if (discrim !== undefined && (m.user.mode === undefined || m.user.mode === discrim.mode) && m.user.name === discrim.name) {
  106. group.push(m)
  107. } else {
  108. if (group.length !== 0)
  109. groups.push({
  110. user: discrim,
  111. type: type ?? "message",
  112. messages: group
  113. });
  114. group = [m];
  115. discrim = m.user;
  116. type = m.type;
  117. }
  118. }
  119. if (group.length !== 0) {
  120. if (type === "status") {
  121. groups.push({
  122. user: undefined,
  123. type,
  124. messages: group
  125. })
  126. } else if (discrim !== undefined) {
  127. groups.push({
  128. user: discrim,
  129. type: type ?? "message",
  130. messages: group
  131. });
  132. }
  133. }
  134. return groups;
  135. }
  136. const groups = group(messages);
  137. const render = (c: MessageFrag): JSX.Element => {
  138. if (c.type === "text") {
  139. return JSX.createTextNode(escapeHtmlShitty(c.content));
  140. } else if (c.type === "url") {
  141. return <a href={c.link}>{escapeHtmlShitty(c.link)}</a>;
  142. } else {
  143. throw "impossible!" // thanks TypeScript
  144. }
  145. }
  146. const Message = ({ message, inclTs }: { message: IrcMessage, inclTs?: boolean }) => {
  147. const prefix = message.type === "action" ? "* " : "";
  148. return <div class={`mg-m mg-${message.type}`}>
  149. {
  150. (inclTs === undefined || inclTs === true) ? <span class="mg-ts"> {message.time} </span> : undefined
  151. }
  152. <span class={`mg-txt mg-${message.type}`}> {prefix} {message.content.map(c => render(c))} </span>
  153. </div>
  154. }
  155. function renderMessageGroup(m: MsgGroup): HtmlEnt {
  156. const u = m.user;
  157. if (u !== undefined) {
  158. return <div class="mg-user">
  159. <div class="mg-pfp">
  160. <img src={`https://offtopia.org/pages/people/${u.name.toLocaleLowerCase()}.jpg`} alt={`${u.name}'s profile picture`} />
  161. </div>
  162. <div class="mg-contents">
  163. <span class="mg-ts-u">
  164. <span class="mg-ts-u-u"> {m.user?.name} </span>
  165. <span class="mg-ts-u-ts"> {m.messages[0].time} </span>
  166. </span>
  167. <Message message={m.messages[0]} inclTs={false} />
  168. {
  169. m.messages.slice(1).map(x =>
  170. <Message message={x} />
  171. )
  172. }
  173. </div>
  174. </div>
  175. } else {
  176. if (m.messages.length < 1) {
  177. return <span />
  178. }
  179. return <div class="mg-no-user">
  180. {m.messages.map(x => <Message message={x} />)}
  181. </div>;
  182. }
  183. }
  184. const rendered = groups.map(renderMessageGroup);
  185. const elem = <html>
  186. <body>
  187. {rendered}
  188. <svg id="squircle-container" width="0" height="0">
  189. <clipPath id="squircle" clipPathUnits="objectBoundingBox">
  190. <path fill="red" stroke="none" d="M 0,0.5 C 0,0 0,0 0.5,0 S 1,0 1,0.5 1,1 0.5,1 0,1 0,0.5" />
  191. </clipPath>
  192. </svg>
  193. </body>
  194. <style>
  195. {readFileSync("style.css")}
  196. </style>
  197. </html>;
  198. console.log(elem.toHtmlStr());