about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/options.js30
-rw-r--r--app/javascript/flavours/glitch/styles/bbcode.scss56
-rw-r--r--app/javascript/flavours/glitch/styles/index.scss2
-rw-r--r--app/javascript/flavours/glitch/styles/monsterpit.scss80
-rw-r--r--app/javascript/styles/application.scss2
-rw-r--r--app/javascript/styles/mastodon/bbcode.scss56
-rw-r--r--app/javascript/styles/mastodon/monsterpit.scss74
-rw-r--r--app/lib/bangtags.rb51
-rw-r--r--app/lib/formatter.rb178
-rw-r--r--app/lib/sanitize_config.rb5
-rw-r--r--app/models/status.rb5
-rw-r--r--config/settings.yml2
14 files changed, 517 insertions, 30 deletions
diff --git a/Gemfile b/Gemfile
index fbf228ff2..aa818123e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -149,3 +149,5 @@ group :production do
 end
 
 gem 'concurrent-ruby', require: false
+
+gem "ruby-bbcode", "~> 2.0"
diff --git a/Gemfile.lock b/Gemfile.lock
index ad0f64e3d..359c748fa 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -533,6 +533,8 @@ GEM
       rainbow (>= 2.2.2, < 4.0)
       ruby-progressbar (~> 1.7)
       unicode-display_width (>= 1.4.0, < 1.7)
+    ruby-bbcode (2.0.3)
+      activesupport (>= 4.2.2)
     ruby-progressbar (1.10.0)
     ruby-saml (1.9.0)
       nokogiri (>= 1.5.10)
@@ -667,7 +669,6 @@ DEPENDENCIES
   brakeman (~> 4.5)
   browser
   bullet (~> 6.0)
-  bundler (~> 1.17)
   bundler-audit (~> 0.6)
   capistrano (~> 3.11)
   capistrano-rails (~> 1.4)
@@ -751,6 +752,7 @@ DEPENDENCIES
   rspec-rails (~> 3.8)
   rspec-sidekiq (~> 3.0)
   rubocop (~> 0.69)
+  ruby-bbcode (~> 2.0)
   sanitize (~> 5.0)
   scss_lint (~> 0.58)
   sidekiq (~> 5.2)
diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js
index 0c94f5514..46b32b4a3 100644
--- a/app/javascript/flavours/glitch/features/compose/components/options.js
+++ b/app/javascript/flavours/glitch/features/compose/components/options.js
@@ -25,6 +25,14 @@ const messages = defineMessages({
     defaultMessage: 'Attach...',
     id: 'compose.attach',
   },
+  bbcode: {
+    defaultMessage: 'BBCode',
+    id: 'compose.content-type.bbcode',
+  },
+  bbdown: {
+    defaultMessage: 'BBdown',
+    id: 'compose.content-type.bbdown',
+  },
   change_privacy: {
     defaultMessage: 'Adjust status privacy',
     id: 'privacy.change',
@@ -232,7 +240,7 @@ class ComposerOptions extends ImmutablePureComponent {
 
     const contentTypeItems = {
       plain: {
-        icon: 'align-left',
+        icon: 'file-text',
         name: 'text/plain',
         text: <FormattedMessage {...messages.plain} />,
       },
@@ -242,10 +250,20 @@ class ComposerOptions extends ImmutablePureComponent {
         text: <FormattedMessage {...messages.html} />,
       },
       markdown: {
-        icon: 'arrow-circle-down',
+        icon: 'hashtag',
         name: 'text/markdown',
         text: <FormattedMessage {...messages.markdown} />,
       },
+      xbbcode: {
+        icon: 'thumb-tack',
+        name: 'text/x-bbcode',
+        text: <FormattedMessage {...messages.bbcode} />,
+      },
+      xbbcodemarkdown: {
+        icon: 'arrow-circle-down',
+        name: 'text/x-bbcode+markdown',
+        text: <FormattedMessage {...messages.bbdown} />,
+      },
     };
 
     //  The result.
@@ -315,11 +333,13 @@ class ComposerOptions extends ImmutablePureComponent {
         {showContentTypeChoice && (
           <Dropdown
             disabled={disabled}
-            icon={(contentTypeItems[contentType.split('/')[1]] || {}).icon}
+            icon={(contentTypeItems[contentType.split('/')[1].replace(/[+-]/g, '')] || {}).icon}
             items={[
-              contentTypeItems.plain,
-              contentTypeItems.html,
+              contentTypeItems.xbbcodemarkdown,
               contentTypeItems.markdown,
+              contentTypeItems.xbbcode,
+              contentTypeItems.html,
+              contentTypeItems.plain,
             ]}
             onChange={onChangeContentType}
             onModalClose={onModalClose}
diff --git a/app/javascript/flavours/glitch/styles/bbcode.scss b/app/javascript/flavours/glitch/styles/bbcode.scss
new file mode 100644
index 000000000..80b2aa57b
--- /dev/null
+++ b/app/javascript/flavours/glitch/styles/bbcode.scss
@@ -0,0 +1,56 @@
+/*
+// Original:
+// https://github.com/computerfairies/mastodon/blob/master/app/javascript/styles/mastodon/bbcode.scss
+*/
+
+.bbcode {
+  &__flip-horizontal {
+    display: inline-block;
+    -webkit-transform: scale(-1, 1);
+    -ms-transform: scale(-1, 1);
+    transform: scale(-1, 1);
+  }
+
+  &__flip-vertical {
+    display: inline-block;
+    -webkit-transform: scale(1, -1);
+    -ms-transform: scale(1, -1);
+    transform: scale(1, -1);
+  }
+
+  @for $i from 1 through 6 {
+    &__size-#{$i} {
+      font-size: #{6 * $i}px;
+
+      & .emojione {
+        width: #{6 * $i}px !important;
+        height:  #{6 * $i}px !important;
+      }
+
+      & .hoverplay {
+        padding-left: #{6 * $i}px !important;
+      }
+    }
+  }
+
+  @for $i from 1 through 2 {
+    &__size-#{$i}:hover {
+      font-size: 12px;
+    }
+  }
+
+  &__left { display: block; text-align: left; }
+  &__center { display: block; text-align: center; }
+  &__right { display: block; text-align: right; }
+  &__lfloat { float: left; }
+  &__rfloat { float: right; }
+  &__spoiler-wrapper {
+    background: black;
+    color: black;
+    padding: 1px 2em 1px 2em;
+  }
+  &__spoiler { color: black; visibility: hidden; }
+  &__spoiler-wrapper:hover > &__spoiler,
+  &__spoiler-wrapper:active > &__spoiler
+  { color: white; visibility: visible; }
+}
diff --git a/app/javascript/flavours/glitch/styles/index.scss b/app/javascript/flavours/glitch/styles/index.scss
index e1c25ac0d..90152c65c 100644
--- a/app/javascript/flavours/glitch/styles/index.scss
+++ b/app/javascript/flavours/glitch/styles/index.scss
@@ -24,3 +24,5 @@
 @import 'accessibility';
 @import 'rtl';
 @import 'dashboard';
+@import 'bbcode';
+@import 'monsterpit';
diff --git a/app/javascript/flavours/glitch/styles/monsterpit.scss b/app/javascript/flavours/glitch/styles/monsterpit.scss
new file mode 100644
index 000000000..7dccd81a4
--- /dev/null
+++ b/app/javascript/flavours/glitch/styles/monsterpit.scss
@@ -0,0 +1,80 @@
+.status__content__text,
+.reply-indicator__content,
+.composer--reply > .content,
+.account__header__content,
+{
+  s { text-decoration: line-through; }
+  del { text-decoration: line-through; }
+  h6 { font-size: 8px; font-weight: bold; }
+  hr { border-color: lighten($dark-text-color, 10%); }
+  sub {
+    vertical-align: sub;
+    font-size: smaller;
+  }
+  sup {
+    vertical-align: super;
+    font-size: smaller;
+  }
+  pre, code {
+    color: lighten($dark-text-color, 33%);
+  }
+  mark {
+    background-color: #ccff15;
+    color: black;
+  }
+  blockquote {
+    font-style: italic;
+  }
+  .caption {
+    display: block;
+    margin: auto;
+    font-size: 12px !important;
+    padding-top: 0;
+    text-align: center;
+    max-width: 80%;
+  }
+  .caption-hidden {
+    display: none;
+  }
+  p.signature {
+    color: lighten($dark-text-color, 20%);
+    font-style: italic;
+    font-size: 12px;
+    text-align: right;
+  }
+}
+
+div.media-caption {
+  p {
+    font-size: 12px !important;
+    margin-bottom: 0;
+    text-align: center;
+  }
+  a {
+		color: $secondary-text-color;
+		text-decoration: none;
+		font-weight: bold;
+
+		&:hover {
+			text-decoration: underline;
+
+			.fa {
+				color: lighten($dark-text-color, 7%);
+			}
+		}
+
+		&.mention {
+			&:hover {
+				text-decoration: none;
+
+				span {
+					text-decoration: underline;
+				}
+			}
+		}
+
+		.fa {
+			color: $dark-text-color;
+		}
+	}
+}
diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss
index 6db3bc3dc..fe3edca47 100644
--- a/app/javascript/styles/application.scss
+++ b/app/javascript/styles/application.scss
@@ -26,3 +26,5 @@
 @import 'mastodon/dashboard';
 @import 'mastodon/rtl';
 @import 'mastodon/accessibility';
+@import 'mastodon/bbcode';
+@import 'mastodon/monsterpit';
diff --git a/app/javascript/styles/mastodon/bbcode.scss b/app/javascript/styles/mastodon/bbcode.scss
new file mode 100644
index 000000000..80b2aa57b
--- /dev/null
+++ b/app/javascript/styles/mastodon/bbcode.scss
@@ -0,0 +1,56 @@
+/*
+// Original:
+// https://github.com/computerfairies/mastodon/blob/master/app/javascript/styles/mastodon/bbcode.scss
+*/
+
+.bbcode {
+  &__flip-horizontal {
+    display: inline-block;
+    -webkit-transform: scale(-1, 1);
+    -ms-transform: scale(-1, 1);
+    transform: scale(-1, 1);
+  }
+
+  &__flip-vertical {
+    display: inline-block;
+    -webkit-transform: scale(1, -1);
+    -ms-transform: scale(1, -1);
+    transform: scale(1, -1);
+  }
+
+  @for $i from 1 through 6 {
+    &__size-#{$i} {
+      font-size: #{6 * $i}px;
+
+      & .emojione {
+        width: #{6 * $i}px !important;
+        height:  #{6 * $i}px !important;
+      }
+
+      & .hoverplay {
+        padding-left: #{6 * $i}px !important;
+      }
+    }
+  }
+
+  @for $i from 1 through 2 {
+    &__size-#{$i}:hover {
+      font-size: 12px;
+    }
+  }
+
+  &__left { display: block; text-align: left; }
+  &__center { display: block; text-align: center; }
+  &__right { display: block; text-align: right; }
+  &__lfloat { float: left; }
+  &__rfloat { float: right; }
+  &__spoiler-wrapper {
+    background: black;
+    color: black;
+    padding: 1px 2em 1px 2em;
+  }
+  &__spoiler { color: black; visibility: hidden; }
+  &__spoiler-wrapper:hover > &__spoiler,
+  &__spoiler-wrapper:active > &__spoiler
+  { color: white; visibility: visible; }
+}
diff --git a/app/javascript/styles/mastodon/monsterpit.scss b/app/javascript/styles/mastodon/monsterpit.scss
new file mode 100644
index 000000000..98d7450ec
--- /dev/null
+++ b/app/javascript/styles/mastodon/monsterpit.scss
@@ -0,0 +1,74 @@
+.status__content__text,
+.reply-indicator__content,
+.composer--reply > .content,
+.account__header__content,
+{
+  s { text-decoration: line-through; }
+  del { text-decoration: line-through; }
+  h6 { font-size: 8px; font-weight: bold; }
+  hr { border-color: lighten($dark-text-color, 10%); }
+  sub {
+    vertical-align: sub;
+    font-size: smaller;
+  }
+  sup {
+    vertical-align: super;
+    font-size: smaller;
+  }
+  pre, code {
+    color: lighten($dark-text-color, 33%);
+  }
+  mark {
+    background-color: #ccff15;
+    color: black;
+  }
+  blockquote {
+    font-style: italic;
+  }
+  .caption {
+    display: block;
+    margin: auto;
+    font-size: 12px !important;
+    padding-top: 0;
+    text-align: center;
+    max-width: 80%;
+  }
+  .caption-hidden {
+    display: none;
+  }
+}
+
+div.media-caption {
+  p {
+    font-size: 12px !important;
+    margin-bottom: 0;
+    text-align: center;
+  }
+  a {
+		color: $secondary-text-color;
+		text-decoration: none;
+		font-weight: bold;
+
+		&:hover {
+			text-decoration: underline;
+
+			.fa {
+				color: lighten($dark-text-color, 7%);
+			}
+		}
+
+		&.mention {
+			&:hover {
+				text-decoration: none;
+
+				span {
+					text-decoration: underline;
+				}
+			}
+		}
+
+		.fa {
+			color: $dark-text-color;
+		}
+	}
+}
diff --git a/app/lib/bangtags.rb b/app/lib/bangtags.rb
index 404d20a0f..4ba6b5e92 100644
--- a/app/lib/bangtags.rb
+++ b/app/lib/bangtags.rb
@@ -26,7 +26,7 @@ class Bangtags
     # list of transformation commands
     @tf_cmds = []
     # list of post-processing commands
-    @post_cmds = [['signature']]
+    @post_cmds = []
     # hash of bangtag variables
     @vars = account.vars
     # keep track of what variables we're appending the value of between chunks
@@ -36,7 +36,7 @@ class Bangtags
   end
 
   def process
-    return unless status.text&.present?
+    return unless status.text&.present? && status.text.include?('#!')
 
     status.text.gsub!('#!!', "#\u200c!")
 
@@ -367,16 +367,19 @@ class Bangtags
             who = cmd[2]
             if who.blank?
               @vars.delete('_they:are')
+              status.footer = nil
               next
             elsif who == 'not'
               who = cmd[3]
               next if who.blank?
               name = who.downcase.gsub(/\s+/, '')
               @vars.delete("_they:are:#{name}")
-              @vars.delete('_they:are') if @vars['_they:are'] == name
+              next unless @vars['_they:are'] == name
+              @vars.delete('_they:are')
+              status.footer = nil
               next
             end
-            name = who.downcase.gsub(/\s+/, '')
+            name = who.downcase.gsub(/\s+/, '').strip
             description = cmd[3..-1].join(':').strip
             if description.blank?
               if @vars["_they:are:#{name}"].nil?
@@ -385,7 +388,8 @@ class Bangtags
             else
               @vars["_they:are:#{name}"] = description
             end
-            @vars['_they:are'] = name.strip
+            @vars['_they:are'] = name
+            status.footer = @vars["_they:are:#{name}"]
           end
         when 'sharekey'
           next if cmd[1].nil?
@@ -401,6 +405,30 @@ class Bangtags
           @vore_stack.push('_draft')
           @component_stack.push(:var)
           add_tags(status, 'self:draft')
+        when 'format', 'type'
+          chunk = nil
+          next if cmd[1].nil?
+          content_types = {
+            't'           => 'text/plain',
+            'txt'         => 'text/plain',
+            'text'        => 'text/plain',
+            'plain'       => 'text/plain',
+            'plaintext'   => 'text/plain',
+
+            'm'           => 'text/markdown',
+            'md'          => 'text/markdown',
+            'markdown'    => 'text/markdown',
+
+            'b'           => 'text/x-bbcode',
+            'bbc'         => 'text/x-bbcode',
+            'bbcode'      => 'text/x-bbcode',
+
+            'bm'          => 'text/x-bbcode+markdown',
+            'bbm'         => 'text/x-bbcode+markdown',
+            'bbdown'      => 'text/x-bbcode+markdown',
+          }
+          v = cmd[1].downcase
+          status.content_type = content_types[c] unless content_types[c].nil?
         when 'visibility'
           chunk = nil
           next if cmd[1].nil?
@@ -421,7 +449,7 @@ class Bangtags
             'world'       => :public,
           }
           v = cmd[1].downcase
-          status.visibility = visibilities[v] if visibilities[v].nil?
+          status.visibility = visibilities[v] unless visibilities[v].nil?
         end
       end
 
@@ -472,17 +500,6 @@ class Bangtags
   def postprocess_before_save
     @post_cmds.each do |post_cmd|
       case post_cmd[0]
-      when 'signature'
-        name = @vars['_they:are']
-        next if name.blank?
-        description = @vars["_they:are:#{name}"]
-        next if description.blank? || @chunks.last(5).join.include?('—')
-        status.local_only = true if Status::LOCAL_ONLY_TOKENS.match?(@chunks.last)
-        if @chunks.first(5).any? { |c| c.strip.match?(/[\r\n]/) || c.lstrip.match?(/^(?:[>#]|```|---|\* |\d+\)|\[\wi+)/) }
-          @chunks << "\n\n[right]— #{description}\u200c[/right]"
-        else
-          @chunks << " [rfloat]— #{description}\u200c[/rfloat]"
-        end
       when 'media'
         media_idx = post_cmd[1]
         media_cmd = post_cmd[2]
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index cb9ca8336..42911b52a 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -30,6 +30,141 @@ class Formatter
 
   include ActionView::Helpers::TextHelper
 
+	BBCODE_TAGS = {
+    :url => {
+			:html_open => '<a href="%url%" rel="noopener nofollow" target="_blank">', :html_close => '</a>',
+			:description => '', :example => '',
+			:allow_quick_param => true, :allow_between_as_param => false,
+			:quick_param_format => /(\S+)/,
+			:quick_param_format_description => 'The size parameter \'%param%\' is incorrect, a number is expected',
+			:param_tokens => [{:token => :url}]
+    },
+		:ul => {
+			:html_open => '<ul>', :html_close => '</ul>',
+			:description => '', :example => '',
+		},
+		:ol => {
+			:html_open => '<ol>', :html_close => '</ol>',
+			:description => '', :example => '',
+		},
+		:li => {
+			:html_open => '<li>', :html_close => '</li>',
+			:description => '', :example => '',
+		},
+		:sub => {
+			:html_open => '<sub>', :html_close => '</sub>',
+			:description => '', :example => '',
+		},
+		:sup => {
+			:html_open => '<sup>', :html_close => '</sup>',
+			:description => '', :example => '',
+		},
+		:h1 => {
+			:html_open => '<h1>', :html_close => '</h1>',
+			:description => '', :example => '',
+		},
+		:h2 => {
+			:html_open => '<h2>', :html_close => '</h2>',
+			:description => '', :example => '',
+		},
+		:h3 => {
+			:html_open => '<h3>', :html_close => '</h3>',
+			:description => '', :example => '',
+		},
+		:h4 => {
+			:html_open => '<h4>', :html_close => '</h4>',
+			:description => '', :example => '',
+		},
+		:h5 => {
+			:html_open => '<h5>', :html_close => '</h5>',
+			:description => '', :example => '',
+		},
+		:h6 => {
+			:html_open => '<h6>', :html_close => '</h6>',
+			:description => '', :example => '',
+		},
+		:abbr => {
+			:html_open => '<abbr>', :html_close => '</abbr>',
+			:description => '', :example => '',
+		},
+		:hr => {
+			:html_open => '<hr>', :html_close => '</hr>',
+			:description => '', :example => '',
+		},
+		:b => {
+			:html_open => '<strong>', :html_close => '</strong>',
+			:description => '', :example => '',
+		},
+		:i => {
+			:html_open => '<em>', :html_close => '</em>',
+			:description => '', :example => '',
+		},
+		:flip => {
+			:html_open => '<span class="bbcode__flip-%direction%">', :html_close => '</span>',
+			:description => '', :example => '',
+			:allow_quick_param => true, :allow_between_as_param => false,
+			:quick_param_format => /(h|v)/,
+			:quick_param_format_description => 'The size parameter \'%param%\' is incorrect, a number is expected',
+			:param_tokens => [{:token => :direction}]
+    },
+		:size => {
+			:html_open => '<span class="bbcode__size-%size%">', :html_close => '</span>',
+			:description => '', :example => '',
+			:allow_quick_param => true, :allow_between_as_param => false,
+			:quick_param_format => /([1-6])/,
+			:quick_param_format_description => 'The size parameter \'%param%\' is incorrect, a number is expected',
+			:param_tokens => [{:token => :size}]
+    },
+		:quote => {
+			:html_open => '<blockquote>', :html_close => '</blockquote>',
+			:description => '', :example => '',
+    },
+		:kbd => {
+			:html_open => '<pre><code>', :html_close => '</code></pre>',
+			:description => '', :example => '',
+    },
+		:code => {
+			:html_open => '<pre>', :html_close => '</pre>',
+			:description => '', :example => '',
+    },
+		:u => {
+			:html_open => '<u>', :html_close => '</u>',
+			:description => '', :example => '',
+    },
+		:s => {
+			:html_open => '<s>', :html_close => '</s>',
+			:description => '', :example => '',
+    },
+		:del => {
+			:html_open => '<del>', :html_close => '</del>',
+			:description => '', :example => '',
+    },
+		:left => {
+			:html_open => '<span class="bbcode__left">', :html_close => '</span>',
+			:description => '', :example => '',
+    },
+		:center => {
+			:html_open => '<span class="bbcode__center">', :html_close => '</span>',
+			:description => '', :example => '',
+    },
+		:right => {
+			:html_open => '<span class="bbcode__right">', :html_close => '</span>',
+			:description => '', :example => '',
+    },
+		:lfloat => {
+			:html_open => '<span class="bbcode__lfloat">', :html_close => '</span>',
+			:description => '', :example => '',
+    },
+		:rfloat => {
+			:html_open => '<span class="bbcode__rfloat">', :html_close => '</span>',
+			:description => '', :example => '',
+    },
+		:spoiler => {
+			:html_open => '<span class="bbcode__spoiler-wrapper"><span class="bbcode__spoiler">', :html_close => '</span></span>',
+			:description => '', :example => '',
+    },
+	}
+
   def format(status, **options)
     if status.reblog?
       prepend_reblog = status.reblog.account.acct
@@ -57,15 +192,26 @@ class Formatter
 
     html = raw_content
     html = "RT @#{prepend_reblog} #{html}" if prepend_reblog
-    html = format_markdown(html) if status.content_type == 'text/markdown'
-    html = encode_and_link_urls(html, linkable_accounts, keep_html: %w(text/markdown text/html).include?(status.content_type))
+
+    case status.content_type
+    when 'text/markdown'
+      html = format_markdown(html)
+    when 'text/x-bbcode'
+      html = format_bbcode(html)
+    when 'text/x-bbcode+markdown'
+      html = format_bbdown(html)
+    end
+
+    html = encode_and_link_urls(html, linkable_accounts, keep_html: %w(text/markdown text/x-bbcode text/x-bbcode+markdown text/html).include?(status.content_type))
     html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify]
 
-    unless %w(text/markdown text/html).include?(status.content_type)
+    unless %w(text/markdown text/x-bbcode text/x-bbcode+markdown text/html).include?(status.content_type)
       html = simple_format(html, {}, sanitize: false)
       html = html.delete("\n")
     end
 
+    html = append_footer(html, status.footer)
+
     html.html_safe # rubocop:disable Rails/OutputSafety
   end
 
@@ -74,6 +220,19 @@ class Formatter
     html.delete("\r").delete("\n")
   end
 
+  def format_bbcode(html, sanitize = true)
+    html = bbcode_formatter(html)
+    html = html.gsub(/<hr>.*<\/hr>/im, '<hr />')
+    return html unless sanitize
+    html = reformat(html)
+    html.delete("\n")
+  end
+
+  def format_bbdown(html)
+    html = format_bbcode(html, false)
+    format_markdown(html)
+  end
+
   def reformat(html)
     sanitize(html, Sanitize::Config::MASTODON_STRICT)
   end
@@ -134,6 +293,19 @@ class Formatter
 
   private
 
+  def append_footer(html, footer)
+    return html if footer.blank?
+    "#{html.strip}<p class=\"signature\">— #{encode(footer)}</p>"
+  end
+
+  def bbcode_formatter(html)
+    begin
+      html = html.bbcode_to_html(false, BBCODE_TAGS, :enable, *BBCODE_TAGS.keys)
+    rescue Exception => e
+    end
+    html
+  end
+
   def markdown_formatter
     return @markdown_formatter if defined?(@markdown_formatter)
 
diff --git a/app/lib/sanitize_config.rb b/app/lib/sanitize_config.rb
index db6f50ed1..9756f2ef6 100644
--- a/app/lib/sanitize_config.rb
+++ b/app/lib/sanitize_config.rb
@@ -14,6 +14,8 @@ class Sanitize
         next true if e =~ /^(h|p|u|dt|e)-/ # microformats classes
         next true if e =~ /^(mention|hashtag)$/ # semantic classes
         next true if e =~ /^(ellipsis|invisible)$/ # link formatting classes
+        next true if e =~ /^bbcode__([a-z1-6\-]+)$/ # bbcode
+        next true if e == 'signature'
       end
 
       node['class'] = class_list.join(' ')
@@ -23,10 +25,11 @@ class Sanitize
       elements: %w(p br span a abbr del pre sub sup blockquote code b strong u i em h1 h2 h3 h4 h5 h6 ul ol li hr),
 
       attributes: {
-        'a'          => %w(href rel class title),
+        'a'          => %w(href rel class title alt),
         'span'       => %w(class),
         'abbr'       => %w(title),
         'blockquote' => %w(cite),
+        'p'          => %w(class),
       },
 
       add_attributes: {
diff --git a/app/models/status.rb b/app/models/status.rb
index 0de92e0c6..895ac8fd6 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -23,11 +23,12 @@
 #  in_reply_to_account_id :bigint(8)
 #  local_only             :boolean
 #  poll_id                :bigint(8)
-#  content_type           :string
 #  tsv                    :tsvector
 #  curated                :boolean          default(FALSE), not null
 #  sharekey               :string
 #  network                :boolean          default(FALSE), not null
+#  content_type           :string
+#  footer                 :text
 #
 
 class Status < ApplicationRecord
@@ -81,7 +82,7 @@ class Status < ApplicationRecord
   validates_with DisallowedHashtagsValidator
   validates :reblog, uniqueness: { scope: :account }, if: :reblog?
   validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog?
-  validates :content_type, inclusion: { in: %w(text/plain text/markdown text/html) }, allow_nil: true
+  validates :content_type, inclusion: { in: %w(text/plain text/markdown text/x-bbcode text/x-bbcode+markdown text/html) }, allow_nil: true
 
   accepts_nested_attributes_for :poll
 
diff --git a/config/settings.yml b/config/settings.yml
index 0e6e97647..fc2233c9f 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -64,7 +64,7 @@ defaults: &defaults
   show_known_fediverse_at_about_page: true
   show_reblogs_in_public_timelines: false
   show_replies_in_public_timelines: false
-  default_content_type: 'text/plain'
+  default_content_type: 'text/x-bbcode+markdown'
 
 development:
   <<: *defaults